<?php
require_once 'File.php';

/**
 * 유틸리티 - 파일/디렉토리
 *
 * 참고 : 이 유틸리티는 PEAR\File.php(1.3.0) 를 참조합니다.
 *
 * <code>
 * // utilFile Instance
 * $utilFile = utilFile::getInstance();
 *
 * // 디렉토리 생성
 * $this->utilFile->mkdir('/user_dir');
 *
 * // 파일 쓰기(디렉토리 자동 생성)
 * $this->utilFile->write('/user_dir/file.txt', 'AAA');
 *
 * // 디렉토리 삭제
 * $this->utilFile->rmdir('/user_dir');
 *
 * // 모든 자원 닫기
 * $this->utilFile->closeAll();
 * </code>
 *
 * @link http://pear.php.net/package/File
 *
 * @package    util
 *
 * @author     jylee3@simplexi.com
 * @version 1.0
 */
class utilFile extends File
{
    /**
     * utilFile Class Instance
     *
     * @return utilFile
     */
    public static function getInstance()
    {
        return utilSplClass::load('utilFile');
    }

    /**
     * 지정된 디렉토리에 대한 파일 및 디렉토리 검색
     *
     * <code>
     * $utilFile = utilFile::getInstance();
     * $iResult = $utilFile->getDirFileFind('/', $aMatch, "*.txt");
     * </code>
     *
     * @param    String    $sDirPath    검색 디렉토리
     * @param    Array    $aMatch    매칭 결과(키:파일=숫자, 디렉토리=경로문자열, 값:파일=파일문자열, 디렉토리=배열)
     * @param    String    $sPattern    검색 패턴(GLOB 패턴 사용)
     *
     * @return    Mixed    Integer = 검색된 파일 및 디렉토리 결과(지정된 디렉토리 최상위), False = 실패
     */
    public function getDirFileFind($sDirPath, &$aMatch, $sPattern="*")
    {
        $sDirPath = $this->getFileDirPathDirectorySeparator($sDirPath);

        $aFileDir = glob($sDirPath.DS.$sPattern);

        if($aFileDir===false || empty($aFileDir))    return false;

        foreach ($aFileDir as $sEntry) {
            if (is_dir($sEntry)) {
                $aMatch[$sEntry] = array();

                $this->getDirFileFind($sEntry, $aMatch[$sEntry], $sPattern);
            } else if (is_file($sEntry)) {
                $aPathInfo = pathinfo($sEntry);

                $aMatch[] = $aPathInfo['basename'];
            }
        }

        return (empty($aMatch) || !is_array($aMatch))? false : sizeof($aMatch);
    }

    /**
     * 디렉토리 구분자 반환
     *
     * <code>
     * $utilFile = utilFile::getInstance();
     * $utilFile->getDirSeparator();
     * </code>
     *
     * @return    Char
     */
    public function getDirSeparator()
    {
        return DS;
    }

    /**
     * 디렉토리 경로 문자열 구분자 치환
     *
     * <code>
     * $utilFile = utilFile::getInstance();
     * $utilFile->getFileDirPathDirectorySeparator('/user_dir/file.txt');
     * </code>
     *
     * @param    String    $sFileDirPath    디렉토리 문자열
     *
     * @return     String    시스템의 디렉토리 구분자로 치환된 디렉토리 문자열
     */
    public function getFileDirPathDirectorySeparator($sFileDirPath)
    {
        $sFileDirPath = str_replace("/" , DS, $sFileDirPath);
        $sFileDirPath = str_replace("\\" , DS, $sFileDirPath);

        return $sFileDirPath;
    }

    /**
     * 디렉토리 생성
     *
     * <code>
     * $utilFile = utilFile::getInstance();
     * $utilFile->mkdir('/user_dir');
     * </code>
     *
     * @param    String    $sDirPath    디렉토리 경로
     * @param    String    $iMode    권한 모드(기본 0777)
     *
     * @return    Boolean    성공시 true, 실패시 false
     */
    public function mkdir($sDirPath, $iMode=0777)
    {
        $sDirPath = $this->getFileDirPathDirectorySeparator($sDirPath);

        if (is_dir($sDirPath))    return true;

        $aDir = explode(DS, $sDirPath);
        $iDir = sizeof($aDir);

        $sPath = '';

        for ($i=0; $i<$iDir; $i++) {
            if ($i==0)    $sPath = $aDir[$i];
            else          $sPath = $sPath.DS.$aDir[$i];

            if (empty($sPath))    continue;

            if (!is_dir($sPath) && !@mkdir($sPath, $iMode)) {
                return false;
            }
        }

        return true;
    }

    /**
     * 디렉토리 복사
     *
     * <code>
     * $utilFile = utilFile::getInstance();
     * $utilFile->copydir('/user_dir', '/test');
     * </code>
     *
     * @param String $sSourcePath    원본 디렉토리 경로
     * @param String $sDestPath    대상 디렉토리 경로
     *
     * @return Boolean
     */
    public function copydir($sSourcePath, $sDestPath)
    {
        if (!is_dir($sSourcePath))            return false;
        if (!is_dir($sDestPath)) {
            if (!$this->mkdir($sDestPath))    return false;
        }

        $sSourcePath = $this->getFileDirPathDirectorySeparator($sSourcePath);
        $rCurrentDirPath = @opendir($sSourcePath);

        if($rCurrentDirPath===false)    return false;

        while (false !== ($sEntryname = readdir($rCurrentDirPath))) {
            if ($sEntryname == "." || $sEntryname == "..")    continue;

            if (is_dir($sSourcePath.DS.$sEntryname)) {
                if (!$this->mkdir($sSourcePath.DS.$sEntryname,
                    $sDestPath.DS.$sEntryname)) {
                    return false;
                }

                if (!$this->copydir($sSourcePath.DS.$sEntryname, $sDestPath.DS.$sEntryname)) {
                    return false;
                }
            } else if (is_file($sSourcePath.DS.$sEntryname)) {
                if (!copy($sSourcePath.DS.$sEntryname, $sDestPath.DS.$sEntryname)) {
                    return false;
                }
            }
        }

        closedir($rCurrentDirPath);

        return is_dir($sDestPath);
    }

    /**
     * 디렉토리 이동
     *
     * <code>
     * $utilFile = utilFile::getInstance();
     * $utilFile->movedir('/user_dir', '/test');
     * </code>
     *
     * @param String $sSourcePath    원본 디렉토리 경로
     * @param String $sDestPath    대상 디렉토리 경로
     *
     * @return Boolean
     */
    public function movedir($sSourcePath, $sDestPath)
    {
        $bResult = $this->copydir($sSourcePath, $sDestPath);

        if ($bResult!==false) {
            return $this->rmdir($sSourcePath);
        } else {
            return false;
        }
    }

    /**
     * 디렉토리 삭제(하위 모든 파일 및 디렉토리 포함)
     *
     * <code>
     * $utilFile = utilFile::getInstance();
     * $utilFile->rmdir('/user_dir');
     * </code>
     *
     * @param    String    $sDirPath    삭제할 디렉토리 경로
     *
     * @return    Boolean    성공시 true, 실패시 false
     */
    public function rmdir($sDirPath)
    {
        if (!is_dir($sDirPath))    return false;

        $sDirPath = $this->getFileDirPathDirectorySeparator($sDirPath);
        $rCurrentDirPath = @opendir($sDirPath);

        if($rCurrentDirPath===false)    return false;

        while (false !== ($sEntryname = readdir($rCurrentDirPath))) {
            if ($sEntryname == "." || $sEntryname == "..")    continue;

            if (is_dir($sDirPath.DS.$sEntryname)) {
                $this->rmdir($sDirPath.DS.$sEntryname);
            } else if (is_file($sDirPath.DS.$sEntryname)) {
                unlink($sDirPath.DS.$sEntryname);
            }
        }

        closedir($rCurrentDirPath);
        rmdir($sDirPath);

        return !is_dir($sDirPath);
    }

    /**
     * 파일 쓰기
     *
     * 참고 : 이 기능은 PEAR의 File의 기능을 확장하였습니다.
     *
     * <code>
     * $utilFile = utilFile::getInstance();
     * $utilFile->write('/user_dir/file.txt', 'AAA');
     * </code>
     *
     * @param     String    $sFilename    데이터를 기록할 파일이름 및 경로
     * @param     String    $sData    파일에 기록할 데이터
     * @param     String    $sMode    파일 쓰기 방식
     * @param     Mixed    $mLock     파일 락 방식(false=사용안함)
     *
     * @return    Mixed    PEAR_Error on error or number of bytes written to file.
     *
     * @see File::write()
     */
    public function write($sFileName, $sData, $sMode=FILE_MODE_APPEND, $mLock=false)
    {
        $aPathInfo = pathinfo($this->getFileDirPathDirectorySeparator($sFileName));

        if (!is_dir($aPathInfo['dirname'])) {
            $this->mkdir($aPathInfo['dirname']);
        }

        return parent::write($sFileName, $sData, $sMode, $mLock);
    }

    /**
     * 파일 다운로드(한계치 4GB)
     *
     * <code>
     * $utilFile = utilFile::getInstance();
     * $bResult = $utilFile->fileDownload('/home/download.txt', '사용자.txt');
     * </code>
     *
     * @param    String    $sFileName    다운로드 대상 파일 경로 및 이름 (eg. /home/download.txt)
     * @param    String    $sDownloadFileName    다운로드시 파일 표시 이름(eg. 사용자.txt)
     *
     * @return    Boolean    true(성공) or false(실패)
     */
    public function fileDownload($sFileName, $sDownloadFileName=null)
    {
        set_time_limit(0);

        $aPathInfo = pathinfo($this->getFileDirPathDirectorySeparator($sFileName));

        $sPath = utilXssVar::filterPath($aPathInfo['dirname']);
        $sFileName = utilXssVar::filterFileName($aPathInfo['basename']);
        $sFileFullPath = $sPath . DS . $sFileName;

        if (empty($sDownloadFileName))    $sDownloadFileName = $sFileName;

        if ($sPath!==false && $sFileName!==false && file_exists($sFileFullPath)) {
            $sExtType = false;

            switch ($aPathInfo['extension']) {
                case "pdf" : $sExtType = "application/pdf";                 break;
                case "exe" : $sExtType = "application/octet-stream";        break;
                case "zip" : $sExtType = "application/zip";                 break;
                case "doc" : $sExtType = "application/msword";              break;
                case "xls" : $sExtType = "application/vnd.ms-excel";        break;
                case "ppt" : $sExtType = "application/vnd.ms-powerpoint";   break;
                case "gif" : $sExtType = "image/gif";                       break;
                case "png" : $sExtType = "image/png";                       break;
                case "jpeg" :
                case "jpg" : $sExtType = "image/jpg";                       break;
                case "mp3" : $sExtType = "audio/mpeg";                      break;
                case "wav" : $sExtType = "audio/x-wav";                     break;
                case "mpeg" :
                case "mpg" :
                case "mpe" : $sExtType = "video/mpeg";                      break;
                case "mov" : $sExtType = "video/quicktime";                 break;
                case "avi" : $sExtType = "video/x-msvideo";                 break;
                case "php" :
                case "htm" :
                case "html" : throw new exceptionFile("Cannot be used for ".$sDownloadFileName." files");    break;
                default: $sExtType = "application/force-download";          break;
            }

            $iFileSize = filesize($sFileFullPath);
            $iFileSize = sprintf('%u', $iFileSize); //smaller than 4GB

            header("Pragma: public");
            header("Expires: 0");
            header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
            header("Cache-Control: public");
            header("Content-Description: Cafe24 File Transfer");
            header("Content-Type: ".$sExtType);
            header("Content-Disposition: attachment; filename=".$sDownloadFileName.";");
            header("Content-Transfer-Encoding: binary");
            header("Content-Length: ".$iFileSize);

            $this->readfileChunked($sFileFullPath);

            return true;
        } else {
            return false;
        }
    }

    private function readfileChunked($sFileName, $isReturnBytes=true)
    {
        $iChunksize = 1*(1024*1024);
        $buffer = '';
        $cnt =0;

        $handle = fopen($sFileName, 'rb');

        if ($handle === false) {
            return false;
        }

        while (!feof($handle)) {
            $buffer = fread($handle, $iChunksize);

            echo $buffer;
            ob_flush();
            flush();

            if ($isReturnBytes) {
                $cnt += strlen($buffer);
            }
        }

        $status = fclose($handle);

        if ($isReturnBytes && $status) {
            return $cnt;
        }

        return $status;
    }

}